---
layout: layouts/page.njk
title: Dropdown Menu
description: Displays a menu to the user — such as a set of actions or functions — triggered by a button.
toc:
  - label: Usage
    id: usage
    children:
      - label: HTML + JavaScript
        id: usage-html-js
        children:
          - label: "Step 1: Include the JavaScript files"
            id: usage-html-js-1
          - label: "Step 2: Add your dropdown menu HTML"
            id: usage-html-js-2
          - label: HTML structure
            id: usage-html-js-3
          - label: JavaScript events
            id: usage-html-js-4
      - label: Jinja and Nunjucks
        id: usage-macro
  - label: Examples
    id: examples
    children:
      - label: Checkboxes
        id: example-checkboxes
      - label: Radio Group
        id: example-radio-group
---

{% from "macros/code_block.njk" import code_block %}
{% from "macros/code_preview.njk" import code_preview %}
{% from "dropdown-menu.njk" import dropdown_menu %}

{% set code_html %}
{% call dropdown_menu(
  id="demo-dropdown-menu",
  trigger="Open",
  trigger_attrs={"class": "btn-outline"},
  popover_attrs={"class": "min-w-56"}
) %}
<div role="group" aria-labelledby="account-options">
  <div role="heading" id="account-options">My Account</div>
  <div role="menuitem">
    Profile
    <span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
  </div>
  <div role="menuitem">
    Billing
    <span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
  </div>
  <div role="menuitem">
    Settings
    <span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
  </div>
  <div role="menuitem">
    Keyboard shortcuts
    <span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘K</span>
  </div>
</div>
<hr role="separator">
<div role="menuitem">
  GitHub
</div>
<div role="menuitem">
  Support
</div>
<div role="menuitem" aria-disabled="true">
  API
</div>
<hr role="separator">
<div role="menuitem">
  Logout
  <span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
{% endcall %}
{% endset %}

{{ code_preview("dropdown-menu", code_html | prettyHtml) }}

<h2 id="usage">Usage</h2>

<h3 id="usage-html-js"><a href="#usage-html-js">HTML + JavaScript</a></h3>

<h4 id="usage-html-js-1"><a href="#usage-html-js-1">Step 1: Include the JavaScript files</a></h4>

<section class="prose">
  <p>You can either <a href="/installation/#install-cdn-all">include the JavaScript file for all the components</a>, or just the one for this component by adding this to the <code>&lt;head&gt;</code> of your page:</p>
</section>

{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/dropdown-menu.min.js" defer></script>{% endset %}
{{ code_block(code_script | prettyHtml, "html") }}

<div class="flex flex-wrap gap-2 my-6">
  <a class="badge-outline" href="/installation/#install-js">
    Components with JavaScript
    {% lucide "arrow-right" %}
  </a>
  <a class="badge-outline" href="/installation/#install-cli">
    Use the CLI
    {% lucide "arrow-right" %}
  </a>
  <a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/js/dropdown-menu.js" target="_blank">
    dropdown-menu.js
    {% lucide "arrow-right" %}
  </a>
</div>

<h4 id="usage-html-js-2"><a href="#usage-html-js-2">Step 2: Add your dropdown menu HTML</a></h4>

{{ code_block(code_html | prettyHtml, "html") }}

<h4 id="usage-html-js-3"><a href="#usage-html-js-3">HTML structure</a></h4>

<section class="prose">
  <dl>
    <dt><code class="highlight language-html">&lt;div class="dropdown-menu"&gt;</code></dt>
    <dd>Wraps around the entire component.
      <dl>
        <dt><code class="highlight language-html">&lt;button type="button" popovertarget="{ POPOVER_ID }"&gt;</code></dt>
        <dd>
          <p>The trigger to open the popover, can also have the following attributes:</p>
          <ul>
            <li><code>id="{BUTTON_ID}"</code>: linked to by the <code>aria-labelledby</code> attribute of the listbox.</li>
            <li><code>aria-haspopup="menu"</code>: indicates that the button opens a menu.</li>
            <li><code>aria-controls="{ MENU_ID }"</code>: points to the menu's id.</li>
            <li><code>aria-expanded="false"</code>: tracks the popover's state.</li>
          </ul>
        </dd>
        <dt><code class="highlight language-html">&lt;div popover class="popover" id="{ POPOVER_ID }"&gt;</code></dt>
        <dd>As with the <a href="/components/popover">Popover</a> component, you can set up the side and alignment of the popover using the <code>data-side</code> and <code>data-align</code> attributes.
        <dl>
          <dt><code class="highlight language-html">&lt;div role="menu"&gt;</code></dt>
          <dd>The menu containing the options. Should have the following attributes:
            <ul>
              <li><code>id="{ MENU_ID }"</code>: refered to by the <code>aria-controls</code> attribute of the trigger.</li>
              <li><code>aria-labelledby="{ BUTTON_ID }"</code>: linked to by the button's <code>id</code> attribute.</li>
            </ul>
            <dl>
              <dt><code class="highlight language-html">&lt;button role="menuitem"&gt;</code></dt>
              <dd>Menu item.</dd>
              <dt><code class="highlight language-html">&lt;button role="menuitemcheckbox"&gt;</code></dt>
              <dd>Menu item with a checkbox.</dd>
              <dt><code class="highlight language-html">&lt;button role="menuitemradio"&gt;</code></dt>
              <dd>Menu item with a radio button.</dd>
              <dt><code class="highlight language-html">&lt;hr role="separator"&gt;</code> <span class="badge-secondary">Optional</span></dt>
              <dd>Separator between groups/options.</dd>
              <dt><code class="highlight language-html">&lt;div role="group"&gt;</code> <span class="badge-secondary">Optional</span></dt>
              <dd>Group of options, can have a <code>aria-labelledby</code> attribute to link to a heading.</dd>
              <dt><code class="highlight language-html">&lt;span role="heading"&gt;</code></dt>
              <dd>Group heading, must have an <code>id</code> attribute if you use the <code>aria-labelledby</code> attribute on the group.</dd>
            </dl>
          </dd>
        </dl>
      </dl>
    </dd>
  </dl>
</section>

<h4 id="usage-html-js-4"><a href="#usage-html-js-4">JavaScript events</a></h4>

<section class="prose">
  <dl>
    <dt><code>basecoat:initialized</code></dt>
    <dd>Once the component is fully initialized, it dispatches a custom (non-bubbling) <code>basecoat:initialized</code> event on itself.</dd>
    <dt><code>basecoat:popover</code></dt>
    <dd>When the popover opens, the component dispatches a custom (non-bubbling) <code>basecoat:popover</code> event on <code>document</code>. Other popover components (Combobox, Dropdown Menu, Popover and Select) listen for this to close any open popovers.</dd>
  </dl>
</section>

<h3 id="usage-macro"><a href="#usage-macro">Jinja and Nunjucks</a></h3>

<div class="prose">
  <p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">dropdown_menu()</code> Nunjucks or Jinja macro for this component.</p>
</div>

<div class="flex flex-wrap gap-2 my-6">
  <a class="badge-outline" href="/installation/#install-macros" target="_blank">
    Use Nunjucks or Jinja macros
    {% lucide "arrow-right" %}
  </a>
  <a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/jinja/dropdown-menu.html.jinja" target="_blank">
    Jinja macro
    {% lucide "arrow-right" %}
  </a>
  <a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/dropdown-menu.njk" target="_blank">
    Nunjucks macro
    {% lucide "arrow-right" %}
  </a>
</div>

{% set raw_code %}{% raw %}{% call dropdown_menu(
  id="dropdown-menu",
  trigger="Open",
  trigger_attrs={"class": "btn-outline"},
  popover_attrs={"class": "min-w-56"}
) %}
<div role="group" aria-labelledby="account-options">
  <div role="heading" id="account-options">My Account</div>
  <div role="menuitem">
    Profile
    <span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
  </div>
  <div role="menuitem">
    Billing
    <span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
  </div>
  <div role="menuitem">
    Settings
    <span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
  </div>
  <div role="menuitem">
    Keyboard shortcuts
    <span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘K</span>
  </div>
</div>
<hr role="separator">
<div role="menuitem">
  GitHub
</div>
<div role="menuitem">
  Support
</div>
<div role="menuitem" disabled>
  API
</div>
<hr role="separator">
<div role="menuitem">
  Logout
  <span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
{% endcall %}{% endraw %}{% endset%}
{{ code_block(raw_code, "jinja") }}

<h2 id="examples"><a href="#examples">Examples</a></h2>

<h3 id="example-checkboxes"><a href="#example-checkboxes">Checkboxes</a></h3>

{% set code %}{% call dropdown_menu(
  id="dropdown-menu-checkboxes",
  trigger="Open",
  trigger_attrs={"class": "btn-outline"},
  popover_attrs={"class": "min-w-56"}
) %}
<div role="group" aria-labelledby="account-options">
  <div role="heading" id="account-options">Account Options</div>
  <div role="menuitem">
    {% lucide "user" %}
    Profile
    <span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
  </div>
  <div role="menuitem">
    {% lucide "credit-card" %}
    Billing
    <span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
  </div>
  <div role="menuitem">
    {% lucide "settings" %}
    Settings
    <span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
  </div>
</div>
<hr role="separator">
<div role="group" aria-labelledby="appearance-options">
  <div role="heading" id="appearance-options">Appearance</div>
  <div
    role="menuitemcheckbox"
    aria-checked="true"
    class="group"
  >
    {% lucide "check", {
      "class": "invisible group-aria-checked:visible",
      "aria-hidden": "true",
      "focusable": "false"
    } %}
    Status Bar
    <span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
  </div>
  <div
    role="menuitemcheckbox"
    aria-checked="false"
    class="group"
    aria-disabled="true"
  >
    {% lucide "check", {
      "class": "invisible group-aria-checked:visible",
      "aria-hidden": "true",
      "focusable": "false"
    } %}
    Activity Bar
    <span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
  </div>
  <div
    role="menuitemcheckbox"
    aria-checked="false"
    class="group"
  >
    {% lucide "check", {
      "class": "invisible group-aria-checked:visible",
      "aria-hidden": "true",
      "focusable": "false"
    } %}
    Panel
    <span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
  </div>
</div>
<hr role="separator">
<div role="menuitem">
  {% lucide "log-out" %}
  Logout
  <span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
{% endcall %}
<script>
(() => {
  const dropdownMenu = document.querySelector('#dropdown-menu-checkboxes');
  const checkboxes = dropdownMenu.querySelectorAll('div[role="menuitemcheckbox"]');
  checkboxes.forEach(checkbox => {
    checkbox.addEventListener('click', () => {
      const isChecked = checkbox.getAttribute('aria-checked') === 'true';
      checkbox.setAttribute('aria-checked', !isChecked);
    });
  });
})();
</script>
{% endset %}
{{ code_preview("dropdown-menu-checkboxes", code | prettyHtml) }}

<h3 id="example-radio-group"><a href="#example-radio-group">Radio Group</a></h3>

{% set code %}{% call dropdown_menu(
  id="dropdown-menu-radio-group",
  trigger="Open",
  trigger_attrs={"class": "btn-outline"},
  popover_attrs={"class": "min-w-56"}
) %}
<div role="group" aria-labelledby="position-options">
  <span id="position-options" role="heading">Panel Position</span>
  <hr role="separator">
  <div
    role="menuitemradio"
    aria-checked="false"
    class="group"
  >
    <div class="size-4 flex items-center justify-center">
      <div
        class="size-2 rounded-full bg-foreground invisible group-aria-checked:visible"
        aria-hidden="true",
        focusable="false"
      ></div>
    </div>
    Status Bar
    <span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
  </div>
  <div
    role="menuitemradio"
    aria-checked="true"
    class="group"
  >
    <div class="size-4 flex items-center justify-center">
      <div
        class="size-2 rounded-full bg-foreground invisible group-aria-checked:visible"
        aria-hidden="true",
        focusable="false"
      ></div>
    </div>
    Activity Bar
    <span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
  </div>
  <div
    role="menuitemradio"
    aria-checked="false"
    class="group"
  >
    <div class="size-4 flex items-center justify-center">
      <div
        class="size-2 rounded-full bg-foreground invisible group-aria-checked:visible"
        aria-hidden="true",
        focusable="false"
      ></div>
    </div>
    Panel
    <span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
  </div>
</div>
{% endcall %}
<script>
(() => {
  const dropdownMenu = document.querySelector('#dropdown-menu-radio-group');
  const radioButtons = dropdownMenu.querySelectorAll('div[role="menuitemradio"]');
  radioButtons.forEach(radioButton => {
    radioButton.addEventListener('click', () => {
      radioButtons.forEach(radioButton => {
        radioButton.setAttribute('aria-checked', 'false');
      });
      radioButton.setAttribute('aria-checked', 'true');
    });
  });
})();
</script>
{% endset %}
{{ code_preview("dropdown-menu", code | prettyHtml) }}